package com.jzz.eletuanbackend.util;


import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

/**
 * 授权器，对外提供授权方法
 */
@Component
public class TokenGenerator {

    private static HashMap<String, Lock> lockMap =
            new HashMap<>();

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    public TokenGenerator() {}

    public String getValidAccessToken(String id) {
        // 加锁
        tryLock(id);

        // 判断现有Token是否过期
        // 获取新Token
        StringBuilder token = new StringBuilder();
        lockMap.get(id).checkOrGenerateToken(token, stringRedisTemplate);

        tryRecycle(id);

        return token.toString().substring(0, token.toString().indexOf('-'));
    }

    private synchronized void tryLock(String id) {
        if (lockMap.containsKey(id)) {
            lockMap.get(id).newSession();
        } else {
            lockMap.put(id, Lock.generate(id));
        }
    }

    private synchronized void tryRecycle(String id) {
        if (lockMap.containsKey(id)) {
            lockMap.get(id).tryRecycle(lockMap);
        }
    }

    private static class Lock {
        private String id;
        private int count = 0;

        private static final long EXPIRE_TIME = 7200 * 1000; // 超时时间，单位为ms

        static synchronized Lock generate(String id) {
            Lock lock = new Lock();
            lock.id = id;
            lock.newSession();
            return lock;
        }

        void newSession() {
            count++;
        }

        void decSession() {
            count--;
        }

        void tryRecycle(Map<String, Lock> map) {
            if (count == 0) {
                map.remove(id);
            }
        }

        /**
         * 需要进行双向绑定以方便取值
         *
         * @param token
         * @param stringRedisTemplate
         */
        void checkOrGenerateToken(StringBuilder token, StringRedisTemplate stringRedisTemplate) {
            synchronized (Lock.this) {
                String finalToken = encrypt(id);

                if (stringRedisTemplate.hasKey(TokenValidator.TOKEN_PREFIX + id)) {
                    // 根据ID获取到旧的Token
                    String str = stringRedisTemplate.opsForValue().get(TokenValidator.TOKEN_PREFIX + id);

                    if (str == null || !str.matches("[0-9a-zA-Z]+-[A-Z0-9]+-[0-9]+-[0-9]+")) {
                        // 不是一个Token，则直接新建新Token
                        token.append(finalToken)
                                .append("-")
                                .append(id)
                                .append("-")
                                .append(System.currentTimeMillis())
                                .append("-")
                                .append(EXPIRE_TIME);
                        stringRedisTemplate.opsForValue().set(TokenValidator.TOKEN_PREFIX + id, token.toString(),
                                EXPIRE_TIME, TimeUnit.MILLISECONDS);
                        stringRedisTemplate.opsForValue().set(finalToken, id + "", EXPIRE_TIME, TimeUnit.MILLISECONDS);
                    } else {
                        String[] tmp = str.split("-");
                        long expireTime = Long.parseLong(tmp[2]) + Long.parseLong(tmp[3]);
                        if (expireTime < System.currentTimeMillis()) {
                            // 过期了嗷
                            // 不过新的机制下过期了Redis中就不存在了
                            token.append(finalToken)
                                    .append("-")
                                    .append(id)
                                    .append("-")
                                    .append(System.currentTimeMillis())
                                    .append("-")
                                    .append(EXPIRE_TIME);

                            // 删除旧Token对应的记录
                            if (stringRedisTemplate.hasKey(tmp[0])) {
                                stringRedisTemplate.delete(tmp[0]);
                            }

                            stringRedisTemplate.opsForValue().set(TokenValidator.TOKEN_PREFIX + id, token.toString(),
                                    EXPIRE_TIME, TimeUnit.MILLISECONDS);
                            stringRedisTemplate.opsForValue().set(finalToken, id + "", EXPIRE_TIME, TimeUnit.MILLISECONDS);
                        } else {
                            token.append(str);
                        }
                    }
                } else {
                    // 没有Token记录，则直接新建
                    token.append(finalToken)
                            .append("-")
                            .append(id)
                            .append("-")
                            .append(System.currentTimeMillis())
                            .append("-")
                            .append(EXPIRE_TIME);
                    stringRedisTemplate.opsForValue().set(TokenValidator.TOKEN_PREFIX + id, token.toString(),
                            EXPIRE_TIME, TimeUnit.MILLISECONDS);
                    stringRedisTemplate.opsForValue().set(finalToken, id + "", EXPIRE_TIME, TimeUnit.MILLISECONDS);
                }
                decSession();
            }
        }

        private String encrypt(Object... args) {
            String token = UUID.randomUUID().toString() +
                    Arrays.toString(args);
            try {
                byte[] bytes = MessageDigest.getInstance("MD5").digest(token.getBytes());
                StringBuilder sb = new StringBuilder();
                for (byte b : bytes) {
                    int val = b & 0xff;
                    if (val < 16) {
                        sb.append("0").append(Integer.toHexString(val));
                    } else {
                        sb.append(Integer.toHexString(val));
                    }
                }
                return sb.toString();
            } catch (NoSuchAlgorithmException e) {
                e.printStackTrace();
            }
            return null;
        }
    }

}

